using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Drawing;
using System.Linq;
using SkaldRogueLike.Main.Common;
using SkaldRogueLike.Main.DungeonGenerating.DirectionProcessing;

namespace SkaldRogueLike.Main.DungeonGenerating.MapImplementations
{
    public sealed class Dungeon : Map
    {
        #region Fields and props

        private const int RoomPlacementScoreForAdjacentCorridor = 1;
        private const int RoomPlacementScoreForOverlapCorridor = 3;
        private const int RoomPlacementScoreForOverlapRoom = 100;

        private readonly List<Room> _rooms = new List<Room>();
        public ReadOnlyCollection<Room> Rooms { get { return _rooms.AsReadOnly(); } }

        public List<Point> VisitedCells { get; set; }
        public bool AllCellsVisited { get { return VisitedCells.Count == DungeonCells.Length; } }

        public IEnumerable<Point> CorridorCellLocations
        {
            get
            {
                for (var x = 0; x < Width; x++)
                    for (var y = 0; y < Height; y++)
                        if (this[x, y].IsCorridor)
                            yield return new Point(x, y);
            }
        }

        public Point DownStairs { get; private set; }
        public Point UpStairs { get; private set; }

        #endregion

        public Dungeon(int width, int height) : base(width, height)
        {
            _rooms = new List<Room>();
            InitializeCells();
            VisitedCells = new List<Point>(Width * Height);
        }

        protected override void InitializeCells()
        {
            foreach (var point in CellLocations)
                DungeonCells[point.X, point.Y] = new DungeonCell();
        }

        public void VisitCell(Point point)
        {
            if (IsOutOfMap(point))
                throw new ArgumentException("Out of Dungeon");
            if (this[point].Visited)
                throw new ArgumentException("It is visited");
            this[point].Visited = true;
            VisitedCells.Add(point);
        }
        
        public Point PickUpRandomCellAndVisitIt()
        {
            var point = new Point(StaticRandom.Next(Width - 1), StaticRandom.Next(Height - 1));
            VisitCell(point);
            return point;
        }

        public bool IsAdjacentCellVisited(Point point, Dir dir)
        {
            return this[GetAdjacentPoint(point, dir)].Visited;
        }

        public Point GetRandomVisitedCell(Point point)
        {
            if (VisitedCells.Count <= 1)
                throw new InvalidOperationException("there are less than two visited cells");
            var i = StaticRandom.Next(VisitedCells.Count);
            while (i == VisitedCells.IndexOf(point))
            {
                i = StaticRandom.Next(VisitedCells.Count);
            }
            return VisitedCells[i];
        }

        public IEnumerable<Point> GetDeadEnds()
        {
            return CellLocations.Where(it => this[it].IsDeadEnd);
        }

        public bool IsAdjacentCellCorridor(Point point, Dir dir)
        {
            return HasAdjacentCell(point, dir) && this[GetAdjacentPoint(point, dir)].IsCorridor;
        }

        public int GetRoomPlacementScore(Point point, Room room)
        {
            if (Bounds.Contains(new Rectangle(point, new Size(room.Width, room.Height))))
            {
                var currentScore = 0;
                foreach (var pointOnMap in room.CellLocations.Select(p => new Point(point.X + p.X, point.Y + p.Y)))
                {
                    if (IsAdjacentCellCorridor(pointOnMap, Dir.North))
                        currentScore += RoomPlacementScoreForAdjacentCorridor;
                    if (IsAdjacentCellCorridor(pointOnMap, Dir.East))
                        currentScore += RoomPlacementScoreForAdjacentCorridor;
                    if (IsAdjacentCellCorridor(pointOnMap, Dir.South))
                        currentScore += RoomPlacementScoreForAdjacentCorridor;
                    if (IsAdjacentCellCorridor(pointOnMap, Dir.West))
                        currentScore += RoomPlacementScoreForAdjacentCorridor;
                    if (this[pointOnMap].IsCorridor)
                        currentScore += RoomPlacementScoreForOverlapCorridor;
                    currentScore += 
                        Rooms.Count(it => it.Bounds.Contains(pointOnMap)) * RoomPlacementScoreForOverlapRoom;
                }
                return currentScore;
            }
            return Int16.MaxValue;
        }

        public void PlaceRoom(Point point, Room room)
        {
            room.Move(point);
            foreach (var roomPoint in room.CellLocations)
            {
                var dungeonPoint = new Point(point.X + roomPoint.X, point.Y + roomPoint.Y);

                this[dungeonPoint].SetSide(Dir.North, room[roomPoint].GetSide(Dir.North));
                this[dungeonPoint].SetSide(Dir.East, room[roomPoint].GetSide(Dir.East));
                this[dungeonPoint].SetSide(Dir.South, room[roomPoint].GetSide(Dir.South));
                this[dungeonPoint].SetSide(Dir.West, room[roomPoint].GetSide(Dir.West));

                if ((roomPoint.X == 0) && (HasAdjacentCell(dungeonPoint, Dir.West)))
                    CreateDoorOrWallOnMap(room, dungeonPoint, Dir.West);
                if ((roomPoint.X == room.Width - 1) && (HasAdjacentCell(dungeonPoint, Dir.East)))
                    CreateDoorOrWallOnMap(room, dungeonPoint, Dir.East);
                if ((roomPoint.Y == 0) && (HasAdjacentCell(dungeonPoint, Dir.North)))
                    CreateDoorOrWallOnMap(room, dungeonPoint, Dir.North);
                if ((roomPoint.Y == room.Height - 1) && (HasAdjacentCell(dungeonPoint, Dir.South)))
                    CreateDoorOrWallOnMap(room, dungeonPoint, Dir.South);
            }
            _rooms.Add(room);
        }

        #region Sides changing

        public Point CreateCorridor(Point point, Dir dir)
        {
            this[point].IsCorridor = true;
            this[GetAdjacentPoint(point, dir)].IsCorridor = true;
            return ChangeSideForAdjacentCells(point, dir, Side.Empty);
        }

        public Point CreateWall(Point point, Dir dir)
        {
            return ChangeSideForAdjacentCells(point, dir, Side.Wall);
        }

        public Point CreateDoor(Point point, Dir dir)
        {
            return ChangeSideForAdjacentCells(point, dir, Side.Door);
        }

        private Point ChangeSideForAdjacentCells(Point point, Dir dir, Side side)
        {
            this[point].SetSide(dir, side);
            var adjacentPointInDir = GetAdjacentPoint(point, dir);
            this[adjacentPointInDir].SetSide(DirectionProcessor.GetOppositeDir(dir), side);
            return adjacentPointInDir;
        }

        private void CreateDoorOrWallOnMap(Room room, Point dungeonPoint, Dir dir)
        {
            if (room.Doors[dir])
                CreateWall(dungeonPoint, dir);
            else
            {
                CreateDoor(dungeonPoint, dir);
                room.Doors[dir] = true;
            }
        }

        #endregion

        public void PlaceStairs(Point downPoint, Point upPoint)
        {
            if (this[downPoint].IsRock || this[upPoint].IsRock)
                throw new ArgumentException("Point(s) are not empty");
            if (downPoint == upPoint)
                throw new ArgumentException("downPoint is equal to upPoint: can not put both stairs on one point");
            DownStairs = downPoint;
            UpStairs = upPoint;
        }
    }
}